Introduction

The purpose of this tutorial is to examine neuron morphology.

We can visualise neurons, compare their morphological similarity with NBLAST, and plot them with their synapses.

To do this, we are going to look at one of the data ‘subsets’ we have pre-prepared.

I have subset the synapses and edgelist relevant for a few interesting areas, so let’s use one of those.

You can change which subset and dataset to examine, by changing these two variables:

# Choose dataset and subset
dataset <- "banc_746"
dataset_id <- "banc_746_id"
data_path <- "gs://brain-and-nerve-cord_exports/sjcabs_data"
subset_name <- "front_leg"
neuropil_pattern <- "T1"

# Detect if using GCS or local path
use_gcs <- grepl("^gs://", data_path)

# Setup GCS filesystem if needed
gcs_fs <- if (use_gcs) setup_gcs_filesystem() else NULL
## Authenticating with Google Cloud Storage...
# Construct paths
meta_path <- construct_path(data_path, dataset, "meta")
skeletons_path <- construct_path(data_path, dataset, "skeletons")

# For subsets, construct paths to pre-filtered files
# Extract base dataset name (e.g., "banc" from "banc_746")
dataset_base <- sub("_[0-9]+$", "", dataset)
subset_dir <- file.path(data_path, dataset_base, subset_name)
edgelist_path <- file.path(subset_dir, paste0(dataset, "_", subset_name, "_simple_edgelist.feather"))
synapses_path <- file.path(subset_dir, paste0(dataset, "_", subset_name, "_synapses.feather"))

cat("Dataset:", dataset, "\n")
## Dataset: banc_746
cat("Subset:", subset_name, "\n")
## Subset: front_leg
cat("Using GCS:", use_gcs, "\n")
## Using GCS: TRUE

But by default, let us look at the front leg neuropil!

This is available for BANC, maleCNS and MANC.

Read Meta Data

Let’s start by reading the meta data for our chosen subset:

# Read the edgelist for this subset to get neuron IDs
edgelist <- read_feather_smart(edgelist_path, gcs_filesystem = gcs_fs)
## Reading from GCS: gs://brain-and-nerve-cord_exports/sjcabs_data/banc/front_leg/banc_746_front_leg_simple_edgelist.feather 
## Downloading from GCS... (this may take several minutes for large files)
## Downloaded 10.29 MB from GCS
## Loading into memory with Arrow...
## ✓ Done! Loaded 364696 rows
# Get unique neuron IDs from pre and post columns
subset_ids <- unique(c(edgelist$pre, edgelist$post))
cat("Found", length(subset_ids), "unique neurons in", subset_name, "edgelist\n")
## Found 3614 unique neurons in front_leg edgelist
# Read full meta data using streaming approach
meta_full <- read_feather_smart(meta_path, gcs_filesystem = gcs_fs)
## Reading from GCS: gs://brain-and-nerve-cord_exports/sjcabs_data/banc/banc_746_meta.feather 
## Downloading from GCS... (this may take several minutes for large files)
## Downloaded 9.84 MB from GCS
## Loading into memory with Arrow...
## ✓ Done! Loaded 168793 rows
# Get proofread neuron IDs (key concept from Tutorial 01!)
proofread_ids <- meta_full[[dataset_id]]

# Filter meta for neurons in this subset
meta <- meta_full %>%
  filter(!!sym(dataset_id) %in% subset_ids)

# Rename ID column to 'id' for consistency
meta <- meta %>%
  rename(id = !!sym(dataset_id))

cat("Loaded", nrow(meta), "neurons in", subset_name, "subset\n")
## Loaded 3614 neurons in front_leg subset
cat("(From", nrow(meta_full), "total proofread neurons)\n")
## (From 168793 total proofread neurons)
head(meta)
## # A tibble: 6 × 18
##   id              supervoxel_id region side  hemilineage nerve flow  super_class
##   <chr>           <chr>         <chr>  <chr> <chr>       <chr> <chr> <chr>      
## 1 72057594143307… 760013367595… neck_… right 00A         <NA>  intr… ascending  
## 2 72057594144998… 760013367598… neck_… left  01A         <NA>  intr… ascending  
## 3 72057594149977… 760013367595… neck_… left  01A         <NA>  intr… ascending  
## 4 72057594149512… 760013368272… neck_… left  01A         <NA>  intr… ascending  
## 5 72057594154148… 762828118039… neck_… right 01A         <NA>  intr… ascending  
## 6 72057594157852… 760013366923… neck_… left  01A         <NA>  intr… ascending  
## # ℹ 10 more variables: cell_class <chr>, cell_sub_class <chr>, cell_type <chr>,
## #   neurotransmitter_predicted <chr>, neurotransmitter_score <dbl>,
## #   cell_function <chr>, cell_function_detailed <chr>, body_part_sensory <chr>,
## #   body_part_effector <chr>, status <chr>

Let’s now plot the neuron counts we have, by their super class:

# Count neurons by super_class
counts <- meta %>%
  count(super_class, sort = TRUE)

# Create bar plot
p <- ggplot(counts, aes(x = reorder(super_class, n), y = n, fill = super_class)) +
  geom_col(show.legend = FALSE) +
  geom_text(aes(label = n), hjust = -0.2, size = 3.5) +
  coord_flip() +
  labs(
    title = paste0("Neuron Counts in ", toupper(dataset), " ", subset_name, " Neuropil"),
    x = "Super Class",
    y = "Number of Neurons"
  ) +
  theme_minimal(base_size = 12) +
  theme(
    plot.title = element_text(face = "bold", hjust = 0.5),
    panel.grid.major.y = element_blank()
  )

save_plot(p, paste0(dataset, "_", subset_name, "_neuron_counts"))

That’s quite a lot of neurons, but not too many.

Let’s make a vector of their IDs.

# Extract neuron IDs
neuron_ids <- meta$id
cat("Total neurons:", length(neuron_ids), "\n")
## Total neurons: 3614
# Show first few IDs
head(neuron_ids)
## [1] "720575941433075470" "720575941449981277" "720575941499772066"
## [4] "720575941495128446" "720575941541480573" "720575941578527065"

Read .swc File

A .swc file is a format for encoding neuron ‘skeleton’ structure.

In short, a neuron can be represented as a tree graph, in which each node has a specified “parent”, except the “root”. When possible, we usually “root” the neuron at the soma, i.e. cell body.

We could work with high-resolution mesh data (not in our Google bucket), but for most tasks a skeleton suffices and is faster.

If you would like to see how to read in mesh data for the BANC project, which enables nice visualisations, see here.

Let’s just take one cell type to start, why not DNg12, known to play a role in the control of the front legs during grooming.

We can read its .swc file straight from the skeleton directories (our helper functions work with both directories and .zip archives).

We can visualise it in BANC space, or in its native space if not a BANC neuron. For non-BANC datasets we provide two spaces, because visualising everything in BANC space assists us with co-visualisation. We will see an example of this later.

For now, let’s read and plot that neuron!

# Get all DNg12 neurons
meta_dng12 <- meta %>%
  filter(grepl("DNg12", cell_type))

neuron_ids <- meta_dng12$id
cat("Found", length(neuron_ids), "DNg12 neurons\n")
## Found 16 DNg12 neurons
# Read all neurons using simplified function
neurons <- read_neurons_dir(
  dir_path = skeletons_path,
  neuron_ids = neuron_ids,
  gcs_filesystem = gcs_fs
)
## Downloading 16 SWC files from GCS...
## ✓ Downloaded 16 files
## Reading neurons with nat::read.neurons...
## ✓ Loaded 16 neurons
cat("Loaded", length(neurons), "neurons\n")
## Loaded 16 neurons
# For visualization, we'll use the first neuron
neuron <- neurons[[1]]
selected_id <- names(neurons)[1]

cat("Visualizing neuron", selected_id, "with", nrow(neuron$d), "points\n")
## Visualizing neuron 720575941399380515 with 353 points
summary(neuron)
##   root nodes segments branchpoints endpoints cable.length nTrees
## 1    1   353      233           74       159      1548774      1

We can also use rgl to show the neuron in an interactive 3D window:

# Temporarily switch to rgl for this chunk
options(nat.plotengine = 'rgl')

# Clear any previous 3D plots
clear3d()

# Plot the neuron in 3D with rgl
plot3d(neuron, col = "darkblue", lwd = 2)

# Add axes and labels
axes3d()
title3d(main = paste("Neuron", selected_id),
        xlab = "X (nm)", ylab = "Y (nm)", zlab = "Z (nm)")

# Switch back to plotly as default
options(nat.plotengine = 'plotly')

We can also use the plotly engine with the natverse:

# Plot with plotly (already set as default engine)
nclear3d()
plot3d(neurons[1:3], col = c("darkblue", "darkred", "darkgreen"), lwd = 2)

Fo publication-ready 2D images, see our nat.ggplot libraries and the tutorial in its README.

Read Neuropil Meshes

In our obj folder, we have some useful neuroanatomical objects!

We can also show the mesh of the BANC brains and ventral nerve cord neuropils, to help contextualise this morphology.

You can find its .obj file in our prepared data, and visualise it with our neuron as so.

# Simple example: Read and display multiple neurons with neuropil mesh
# Smaller specific neuropils are in obj/neuropils/ subdirectory

neuropil_mesh_path <- file.path(data_path, dataset_base, "obj", "neuropils",
                                 paste0("banc_neuropil_ITO_midbrain_", neuropil_pattern, "_L_nm.obj"))

# Download and read the mesh
temp_mesh <- tempfile(fileext = ".obj")
system(paste("gsutil cp", neuropil_mesh_path, temp_mesh))
leg_neuropil <- rgl::readOBJ(temp_mesh)

# Visualize multiple neurons with neuropil mesh using plotly
nclear3d()
plot3d(leg_neuropil, col = "lightblue", alpha = 0.3)
plot3d(neurons[1:3], col = c("darkblue", "darkred", "darkgreen"), lwd = 2)
title3d(main = "DNg12 Neurons with Neuropil Mesh")

Let’s fetch both the BANC ventral nerve cord neuropil, and the leg neuropil.

We can show them over our neurons, with a little transparency.

# Construct paths to mesh directories
# Large anatomical regions (VNC, brain, etc.) are in obj/ directory
region_path <- file.path(data_path, dataset_base, "obj")
cat("Large region meshes path:", region_path, "\n")
## Large region meshes path: gs://brain-and-nerve-cord_exports/sjcabs_data/banc/obj
# Smaller specific neuropils are in obj/neuropils/ subdirectory
neuropil_path <- file.path(data_path, dataset_base, "obj", "neuropils")
cat("Neuropil meshes path:", neuropil_path, "\n")
## Neuropil meshes path: gs://brain-and-nerve-cord_exports/sjcabs_data/banc/obj/neuropils
# List available OBJ files in the bucket
region_files <- system(paste("gsutil ls", region_path), intern = TRUE)
neuropil_files <- system(paste("gsutil ls", neuropil_path), intern = TRUE)

# Find VNC and leg neuropil meshes
vnc_obj <- region_files[grepl("vnc.*\\.obj$", region_files, ignore.case = TRUE)][1]
leg_obj <- neuropil_files[grepl(neuropil_pattern, basename(neuropil_files))][1]

cat("VNC mesh:", vnc_obj, "\n")
## VNC mesh: gs://brain-and-nerve-cord_exports/sjcabs_data/banc/obj/banc_vnc_neuropil_nm.obj
cat("Leg mesh:", leg_obj, "\n")
## Leg mesh: gs://brain-and-nerve-cord_exports/sjcabs_data/banc/obj/neuropils/banc_neuropil_COURT_vnc_ProNM-T1_nm.obj
# Download and read meshes
temp_vnc <- tempfile(fileext = ".obj")
temp_leg <- tempfile(fileext = ".obj")

if (!is.na(vnc_obj) && length(vnc_obj) > 0) {
  system(paste("gsutil cp", vnc_obj, temp_vnc))
  vnc_mesh <- rgl::readOBJ(temp_vnc)
}

if (!is.na(leg_obj) && length(leg_obj) > 0) {
  system(paste("gsutil cp", leg_obj, temp_leg))
  leg_mesh <- rgl::readOBJ(temp_leg)
}

# Plot neurons with neuropil meshes using plotly
nclear3d()

# Plot meshes with transparency
if (exists("vnc_mesh")) {
  shade3d(vnc_mesh, col = "lightgrey", alpha = 0.1)
}
if (exists("leg_mesh")) {
  shade3d(leg_mesh, col = "lightblue", alpha = 0.3)
}

# Plot multiple neurons in different colors
n_plot <- min(length(neurons), 5)  # Plot up to 5 neurons
neuron_colors <- c("darkblue", "darkred", "darkgreen", "darkorange", "purple")

cat("Plotting", n_plot, "DNg12 neurons with neuropil context\n")
## Plotting 5 DNg12 neurons with neuropil context
for (i in 1:n_plot) {
  plot3d(neurons[[i]], col = neuron_colors[i], lwd = 2, add = (i > 1))
}

title3d(main = paste(n_plot, "DNg12 Neurons in Neuropil Context"),
        xlab = "X (nm)", ylab = "Y (nm)", zlab = "Z (nm)")

Transform between template spaces

Different connectomic and light-level anatomical data are collected in different individuals, and their data may be “registered” to different template brains.

This plot shows the common template brains, including connectomic datasets, and the transforms that we have precomputed:

Template Brain Bridging Graph

In R, these transforms can be accessed via nat.flybrains

In this workshop, for each dataset we already provide transformed .swc neuron morphology data in the native dataset AND in BANC. Synapse data is given in the native dataset’s space (and may be in raw voxels, nanometers or microns).

This enables you to plot things from multiple datasets in BANC easily.

For example, we can switch to looking at the maleCNS data, and read DNg12 neurons from maleCNS. We can then co-plot them in BANC space, with our BANC DNg12 neurons:

# Switch to maleCNS dataset
dataset_male <- "malecns"
dataset_male_id <- "malecns_09_id"

# Read maleCNS meta data
meta_male_path <- construct_path(data_path, dataset_male, "meta")
meta_male_full <- read_feather_smart(meta_male_path, gcs_filesystem = gcs_fs)

# Filter for DNg12 neurons in maleCNS
meta_male_dng12 <- meta_male_full %>%
  filter(grepl("DNg12", cell_class, ignore.case = TRUE))

cat("Found", nrow(meta_male_dng12), "DNg12 neurons in maleCNS\n")

# Construct path to maleCNS skeletons in BANC space
skeletons_male_banc_path <- construct_path(data_path, dataset_male, "skeletons",
                                           space_suffix = "banc_space")

# Read maleCNS neurons (already in BANC space)
# Sample a few for visualization
n_male_sample <- min(5, nrow(meta_male_dng12))
male_ids <- meta_male_dng12$id[1:n_male_sample]

cat("Reading", n_male_sample, "maleCNS neurons from BANC space...\n")
neurons_male_banc <- read_neurons_dir(
  dir_path = skeletons_male_banc_path,
  neuron_ids = male_ids,
  gcs_filesystem = gcs_fs
)

# Convert to microns
neurons_male_banc_um <- neurons_male_banc / 1000

# Co-plot BANC and maleCNS neurons
nclear3d()

# Plot BANC DNg12 neurons in blue
cat("Plotting BANC neurons...\n")
n_banc_plot <- min(5, length(neurons_um))
for (i in 1:n_banc_plot) {
  plot3d(neurons_um[[i]], col = "darkblue", lwd = 2, add = TRUE)
}

# Plot maleCNS DNg12 neurons in red
cat("Plotting maleCNS neurons...\n")
for (i in 1:length(neurons_male_banc_um)) {
  plot3d(neurons_male_banc_um[[i]], col = "darkred", lwd = 2, add = TRUE)
}

title3d(main = "Co-visualisation: BANC (blue) vs maleCNS (red) DNg12 Neurons",
        xlab = "X (µm)", ylab = "Y (µm)", zlab = "Z (µm)")

However, if you want do do spatial transforms yourself, you will need to use nat.templatebrains and its xform_brain function. A bit confusingly, we will also need bancr to complete the bridge to BANC space.

For example, let’s read the native MANC neurons

# Download template brain registrations (only needs to be done once)
nat.flybrains::download_jefferislab_registrations()

# Switch to MANC dataset
dataset_manc <- "manc"
dataset_manc_id <- "manc_v1_id"

# Read MANC meta data
meta_manc_path <- construct_path(data_path, dataset_manc, "meta")
meta_manc_full <- read_feather_smart(meta_manc_path, gcs_filesystem = gcs_fs)

# Filter for DNg12 neurons in MANC
meta_manc_dng12 <- meta_manc_full %>%
  filter(grepl("DNg12", cell_class, ignore.case = TRUE))

cat("Found", nrow(meta_manc_dng12), "DNg12 neurons in MANC\n")

# Construct path to MANC skeletons in native MANC space
skeletons_manc_path <- construct_path(data_path, dataset_manc, "skeletons",
                                      space_suffix = paste0(dataset_manc, "_space"))

# Read MANC neurons in native space
n_manc_sample <- min(3, nrow(meta_manc_dng12))
manc_ids <- meta_manc_dng12$id[1:n_manc_sample]

cat("Reading", n_manc_sample, "MANC neurons from native space...\n")
manc_dng12 <- read_neurons_dir(
  dir_path = skeletons_manc_path,
  neuron_ids = manc_ids,
  gcs_filesystem = gcs_fs
)

# Transform MANC neurons to JRCVNC2018F template brain
cat("Transforming MANC -> JRCVNC2018F...\n")
manc_dng12_jrcvnc2018f <- xform_brain(manc_dng12,
                                      sample = "MANC",
                                      reference = "JRCVNC2018F")

# Transform from JRCVNC2018F to BANC using bancr
cat("Transforming JRCVNC2018F -> BANC...\n")
manc_dng12_banc <- bancr::banc_to_JRC2018F(manc_dng12_jrcvnc2018f,
                                           region = "vnc",
                                           method = "tpsreg",
                                           banc.units = "nm",
                                           inverse = TRUE)

# Convert to microns
manc_dng12_banc_um <- manc_dng12_banc / 1000

# Co-plot MANC neurons with BANC neurons from earlier
nclear3d()

# Plot BANC DNg12 neurons in blue
cat("Plotting BANC neurons...\n")
n_banc_plot <- min(5, length(neurons_um))
for (i in 1:n_banc_plot) {
  plot3d(neurons_um[[i]], col = "darkblue", lwd = 2, add = TRUE)
}

# Plot transformed MANC DNg12 neurons in green
cat("Plotting transformed MANC neurons...\n")
for (i in 1:length(manc_dng12_banc_um)) {
  plot3d(manc_dng12_banc_um[[i]], col = "darkgreen", lwd = 2, add = TRUE)
}

title3d(main = "Co-visualisation: BANC (blue) vs MANC (green) DNg12 Neurons",
        xlab = "X (µm)", ylab = "Y (µm)", zlab = "Z (µm)")

cat("\nNote: MANC neurons were transformed through MANC -> JRCVNC2018F -> BANC\n")

You can also xform_brain any other data with 3D points, e.g. meshes (mesh3d objects), and data frames or matrices (with x,y and z columns), e.g. synapse data.

NBLAST

Now that we have seen how to visualise the skeleton of one neuron, let’s grab all of our neurons from this neuropil and assess how similar they are.

To read all the neurons, we can select just our subset from the skeleton directory:

# Create list of SWC file names for our neuron IDs
# Limit to first 50 for performance in this example
n_sample <- min(50, length(neuron_ids))

cat("Reading", n_sample, "neurons from skeleton directory...\n")
## Reading 16 neurons from skeleton directory...
neurons <- read_neurons_dir(
  dir_path = skeletons_path,
  neuron_ids = neuron_ids[1:n_sample],
  gcs_filesystem = gcs_fs
)
## Downloading 16 SWC files from GCS...
## ✓ Downloaded 16 files
## Reading neurons with nat::read.neurons...
## ✓ Loaded 16 neurons
cat("Loaded", length(neurons), "neurons\n")
## Loaded 16 neurons

We can now convert them from 1. nm to µm, and 2. to a ‘dotprops’ vector cloud, and use this to run an NBLAST!

# Convert from nm to um (microns)
neurons_um <- neurons / 1000

# Convert to dotprops (point cloud with tangent vectors)
# This removes connectivity but preserves spatial structure
neurons_dp <- dotprops(neurons_um, resample = 1, k = 5)

cat("Converted to dotprops representation\n")

# Run NBLAST all-by-all comparison
# This computes morphological similarity scores
cat("Running NBLAST (this may take a moment)...\n")
# Use smat=NULL to use raw dot product scores
nblast_scores <- nat.nblast::nblast_allbyall(neurons_dp, smat = NULL)

cat("NBLAST complete!\n")
cat("Score matrix dimensions:", dim(nblast_scores), "\n")

Let’s visualise the result as a dendrogram:

# Perform hierarchical clustering on NBLAST scores
nblast_hc <- nhclust(scoremat = nblast_scores)

# Plot and save dendrogram
png(file.path(img_dir, paste0(dataset, "_", subset_name, "_nblast_dendrogram.png")),
    width = 12, height = 8, units = "in", res = 300)
plot(nblast_hc, labels = FALSE,
     main = "Hierarchical Clustering of Neuron Morphology (NBLAST)",
     xlab = "Neurons", ylab = "Height")
dev.off()

# Also display in notebook
plot(nblast_hc, labels = FALSE,
     main = "Hierarchical Clustering of Neuron Morphology (NBLAST)",
     xlab = "Neurons", ylab = "Height")

Great, now let’s view ~20 clusters by morphology:

# Cut the dendrogram into k clusters
k <- 20
clusters <- cutree(nblast_hc, k = k)

cat("Divided neurons into", k, "clusters\n")
table(clusters)

# Create a color palette for clusters
cluster_colors <- rainbow(k)

# Plot neurons colored by cluster
nclear3d()

for (i in 1:length(neurons_um)) {
  cluster_id <- clusters[i]
  plot3d(neurons_um[[i]], col = cluster_colors[cluster_id],
         lwd = 1, add = TRUE)
}

title3d(main = paste(k, "Morphological Clusters"),
        xlab = "X (µm)", ylab = "Y (µm)", zlab = "Z (µm)")

You can try playing with the number of clusters, k.

Cool! What do you think all of these neurons do?

New Dataset (Coming Soon)

Note: This section demonstrates cross-dataset co-visualisation once additional datasets like maleCNS or MANC have their skeleton data uploaded to the bucket. For now, we’ll continue exploring BANC data.

Your Turn: New Subset

Now try this analysis yourself with a different subset, e.g., antennal lobe!

Simply change the subset_name variable at the top of the tutorial and re-run the analysis.

Extension: Axon-Dendrite Splits (Coming Soon)

For all of our datasets, except BANC right now, we have axon-dendrite splits that have been pre-calculated.

This uses a graph theoretic algorithm from Schneider-Mizell et al. 2016 (see the paper).

The method relies on the idea that information runs from input to output synapses, and determines axon versus dendrites by “cutting” the neuron at its major information bottleneck, and observing which half has more input/output.

The label column in the underlying .swc files gives the compartment.

In addition, the pre_label and post_label columns in our synapse tables give compartments.

Note: This section will be available once maleCNS skeleton data with compartment labels is uploaded. Below is the code framework for when that data becomes available.

Session Information

sessionInfo()
## R version 4.2.1 (2022-06-23)
## Platform: x86_64-apple-darwin17.0 (64-bit)
## Running under: macOS Big Sur ... 10.16
## 
## Matrix products: default
## BLAS:   /Library/Frameworks/R.framework/Versions/4.2/Resources/lib/libRblas.0.dylib
## LAPACK: /Library/Frameworks/R.framework/Versions/4.2/Resources/lib/libRlapack.dylib
## 
## locale:
## [1] en_GB.UTF-8/en_GB.UTF-8/en_GB.UTF-8/C/en_GB.UTF-8/en_GB.UTF-8
## 
## attached base packages:
## [1] stats     graphics  grDevices utils     datasets  methods   base     
## 
## other attached packages:
##  [1] duckdb_0.9.2-1     DBI_1.2.3          plotly_4.11.0      nat_1.11.0        
##  [5] rgl_1.2.8          patchwork_1.1.3    forcats_0.5.2      stringr_1.6.0     
##  [9] dplyr_1.1.4        purrr_1.1.0        readr_2.1.5        tidyr_1.3.1       
## [13] tibble_3.3.0       ggplot2_4.0.0.9000 tidyverse_1.3.2    arrow_16.1.0      
## 
## loaded via a namespace (and not attached):
##  [1] fs_1.6.3            natcpp_0.1.0        lubridate_1.8.0    
##  [4] bit64_4.6.0-1       RColorBrewer_1.1-3  httr_1.4.7         
##  [7] tools_4.2.1         backports_1.5.0     bslib_0.6.1        
## [10] utf8_1.2.6          R6_2.6.1            lazyeval_0.2.2     
## [13] withr_3.0.2         tidyselect_1.2.1    bit_4.6.0          
## [16] compiler_4.2.1      extrafontdb_1.0     textshaping_0.3.6  
## [19] cli_3.6.5           rvest_1.0.3         xml2_1.3.6         
## [22] labeling_0.4.3      sass_0.4.8          scales_1.4.0       
## [25] nat.utils_0.6.1     S7_0.2.0            systemfonts_1.2.3  
## [28] nabor_0.5.0         digest_0.6.37       rmarkdown_2.30     
## [31] base64enc_0.1-3     dichromat_2.0-0.1   pkgconfig_2.0.3    
## [34] htmltools_0.5.8.1   extrafont_0.18      dbplyr_2.2.1       
## [37] fastmap_1.2.0       htmlwidgets_1.6.4   rlang_1.1.6        
## [40] readxl_1.4.1        rstudioapi_0.17.1   jquerylib_0.1.4    
## [43] farver_2.1.2        generics_0.1.4      jsonlite_2.0.0     
## [46] crosstalk_1.2.0     googlesheets4_1.1.1 magrittr_2.0.4     
## [49] Matrix_1.6-1.1      Rcpp_1.0.11         reticulate_1.34.0  
## [52] lifecycle_1.0.4     stringi_1.8.3       yaml_2.3.10        
## [55] grid_4.2.1          crayon_1.5.3        lattice_0.20-45    
## [58] haven_2.5.1         hms_1.1.3           knitr_1.50         
## [61] pillar_1.11.1       igraph_1.5.1        codetools_0.2-18   
## [64] reprex_2.0.2        glue_1.8.0          evaluate_1.0.5     
## [67] data.table_1.16.2   modelr_0.1.11       vctrs_0.6.5        
## [70] png_0.1-8           tzdb_0.4.0          Rttf2pt1_1.3.11    
## [73] cellranger_1.1.0    gtable_0.3.6        assertthat_0.2.1   
## [76] cachem_1.1.0        xfun_0.54           broom_1.0.6        
## [79] ragg_1.2.4          filehash_2.4-6      googledrive_2.1.1  
## [82] viridisLite_0.4.2   gargle_1.6.0